﻿namespace ch08.binary.interpreter
{
    public static partial class Instr
    {
        // hack!
        public static void Call(ref Vm vm, object args)
        {
            var idx = (int) ((uint) args);
            var importedFuncCount = vm.Module.ImportSec?.Length ?? 0; // TODO
            if (idx < importedFuncCount)
            {
                callAssertFunc(ref vm, args); // hack!
            }
            else
            {
                callInternalFunc(ref vm, idx - importedFuncCount);
            }
        }

        /*
operand stack:

+~~~~~~~~~~~~~~~+
|               |
+---------------+
|     stack     |
+---------------+
|     locals    |
+---------------+
|     params    |
+---------------+
|  ............ |
*/
        static void callInternalFunc(ref Vm vm, int idx)
        {
            var ftIdx = vm.Module.FuncSec[idx];
            var ft = vm.Module.TypeSec[ftIdx];
            var code = vm.Module.CodeSec[idx];
            vm.EnterBlock(OpCodes.Call, ref ft, ref code.Expr);

            // alloc locals
            var localCount = (int) (code.GetLocalCount());
            for (var i = 0; i < localCount; i++)
            {
                vm.OperandStack.PushU64(0);
            }
        }

        // hack!
        private static void callAssertFunc(ref Vm vm, object args)
        {
            var idx = (uint) args;
            switch (vm.Module.ImportSec[idx].Name)
            {
                case "assert_true":
                    assertEq(vm.OperandStack.PopBool(), true);
                    break;
                case "assert_false":
                    assertEq(vm.OperandStack.PopBool(), false);
                    break;
                case "assert_eq_i32":
                    assertEq(vm.OperandStack.PopU32(), vm.OperandStack.PopU32());
                    break;
                case "assert_eq_i64":
                    assertEq(vm.OperandStack.PopU64(), vm.OperandStack.PopU64());
                    break;
                case "assert_eq_f32":
                    assertEq(vm.OperandStack.PopF32(), vm.OperandStack.PopF32());
                    break;
                case "assert_eq_f64":
                    assertEq(vm.OperandStack.PopF64(), vm.OperandStack.PopF64());
                    break;
                default:
                    Panic.Run("TODO");
                    break;
            }
        }

        private static void assertEq(object a, object b)
        {
            if (!a.Equals(b))
            {
                Panic.Run("{0} != {1} ", a, b);
            }
        }

        private static void brIf(ref Vm vm, object args)
        {
            if (vm.PopBool())
            {
                Br(ref vm, args);
            }
        }

        private static void brTable(ref Vm vm, object args)
        {
            var brTableArgs = (BrTableArgs) args;
            var n = (int) (vm.PopU32());
            if (n < (brTableArgs.Labels.Length))
            {
                Br(ref vm, brTableArgs.Labels[n]);
            }
            else
            {
                Br(ref vm, brTableArgs.Default);
            }
        }

        private static void Return(ref Vm vm, object args)
        {
            var (_, labelIdx) = vm.ControlStack.TopCallFrame();
            Br(ref vm, (uint) (labelIdx));
        }

        private static void unreachable(ref Vm vm, object args)
        {
            Panic.Run(Errors.ErrTrap);
        }

        private static void nop(ref Vm vm, object args)
        {
            // do nothing
        }

        private static void block(ref Vm vm, object args)
        {
            var blockArgs = (BlockArgs) args;
            var bt = vm.Module.GetBlockType(blockArgs.BT);
            vm.EnterBlock(OpCodes.Block, ref bt, ref blockArgs.Instrs);
        }

        private static void Loop(ref Vm vm, object args)
        {
            var blockArgs = (BlockArgs) args;
            var bt = vm.Module.GetBlockType(blockArgs.BT);
            vm.EnterBlock(OpCodes.Loop, ref bt, ref blockArgs.Instrs);
        }

        private static void If(ref Vm vm, object args)
        {
            var ifArgs = (IfArgs) args;
            var bt = vm.Module.GetBlockType(ifArgs.BT);
            if (vm.PopBool())
            {
                vm.EnterBlock(OpCodes.If, ref bt, ref ifArgs.Instrs1);
            }
            else
            {
                vm.EnterBlock(OpCodes.If, ref bt, ref ifArgs.Instrs2);
            }
        }

        private static void Br(ref Vm vm, object args)
        {
            var labelIdx = (int) ((uint) args);
            for (var i = 0; i < labelIdx; i++)
            {
                vm.PopControlFrame();
            }

            // var cf = vm._controlStack.Frames[^1];
            if (vm.ControlStack.Frames[^1].Opcode != OpCodes.Loop)
            {
                vm.ExitBlock();
            }
            else
            {
                vm.ResetBlock(ref vm.ControlStack.Frames[^1]);
                vm.ControlStack.Frames[^1].Pc = 0;
            }
        }
    }
}